/*  PLUTO JS -Scripting Web Server.
    Copyright (C) 2013 vivek gangadharan

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
package pluto.http;


import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URLEncoder;
import java.nio.channels.Channels;
import java.util.StringTokenizer;
import java.util.logging.Level;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import static pluto.core.AppLogger.*;
import pluto.core.ApplicationConfiguration;
import pluto.core.Host;
import pluto.core.Localizer;
import pluto.core.Message;
import pluto.js.JSCallBack;
import pluto.js.JSFunctionExecutor;
import pluto.js.JSScriptletExecutor;

public class HTTPSession implements Runnable,JSCallBack  {

    private final  Socket socket;
    private final ApplicationConfiguration config;
    private final Host host;
    
    public HTTPSession(Host host,ApplicationConfiguration config,Socket socket) {

        this.config=config;
        this.socket = socket;
        this.host= host;
    }

    @Override
    public void run() {
        try {
            HTTPRequest request = new HTTPRequest(socket.getInputStream());
            HTTPResponse response = new HTTPResponse(socket.getOutputStream());
            if (request != null && request.uri() != null) {
                Function function = config.getFunction(request.uri());
                File scpFile = config.jssFile(request.uri());
                if (function != null) {
                    JSFunctionExecutor.callJSFunction(host,config,function, this, request, response);
                    if (response != null) {
                        if (response.sendFile() != null) {
                            findAndSetFile(response, response.sendFile(), request);
                        }
                    }
                } else if (scpFile != null) {
                    JSScriptletExecutor.callJSScriplet(host,config,scpFile, this, request, response);
                } else {
                    findAndSetFile(response, request.uri(), request);
                }
                response.sendResponse();
            }
            request.clean();
            response.clean();
            socket.close();
        } catch (IOException ex) {
            log.log(Level.SEVERE, Localizer.getMessage(Message.ErrorDispatchRequest),ex);
        } catch (Exception ex) {
            log.log(Level.SEVERE, Localizer.getMessage(Message.ErrorDispatchRequest),ex);
        }

    }

    @Override
    public void sucess(Context context, Object result, Object... arg) {
        
    }

    @Override
    public void failed(Context context, Exception e, Object... arg) {
        log.log(Level.SEVERE,"Error executing script",e);
    }
    
    private void findAndSetFile(HTTPResponse res, String uri, HTTPRequest req) {
        File homeDir = config.web();

        // Make sure we won't die of an exception later
        if (!homeDir.isDirectory()) {
            response(HTTPDefinition.HTTPStatus.INTERNALERROR, HTTPDefinition.MIMEType.MIME_PLAINTEXT,
                    Localizer.getMessage(Message.HTTPFileNotFound,req.uri()), res);
            return;
        }


        uri = uri.trim().replace(File.separatorChar, '/');
        if (uri.indexOf('?') >= 0) {
            uri = uri.substring(0, uri.indexOf('?'));
        }

        if (uri.startsWith("..") || uri.endsWith("..") || uri.indexOf("../") >= 0 && !res.isIsFileDispatch()) {
            response(HTTPDefinition.HTTPStatus.INTERNALERROR, HTTPDefinition.MIMEType.MIME_PLAINTEXT,Localizer.getMessage(Message.FileAccessRestricited), res);
            return;
        }


        File f = new File(homeDir, uri);
        if (!f.exists()) {
            response(HTTPDefinition.HTTPStatus.INTERNALERROR, HTTPDefinition.MIMEType.MIME_PLAINTEXT,
                    Localizer.getMessage(Message.HTTPFileNotFound,uri), res);
            return;
        }

        if (f.isDirectory()) {
            if (!uri.endsWith("/")) {
                uri += "/";
                response(HTTPDefinition.HTTPStatus.REDIRECT, HTTPDefinition.MIMEType.MIME_HTML,
                        "<html><body>Redirected: <a href=\"" + uri + "\">"
                        + uri + "</a></body></html>", res);
                res.header("Location", uri);
                return;
            }

            if (new File(f, "index.html").exists()) {
                f = new File(homeDir, uri + "index.html");
            } else if (new File(f, "index.htm").exists()) {
                f = new File(homeDir, uri + "index.htm");
            } 
            else if (config.allowDirListing() && f.canRead()) {
                String[] files = f.list();
                String msg = "<html><body><h1>Directory " + uri + "</h1><br/>";

                if (uri.length() > 1) {
                    String u = uri.substring(0, uri.length() - 1);
                    int slash = u.lastIndexOf('/');
                    if (slash >= 0 && slash < u.length()) {
                        msg += "<b><a href=\"" + uri.substring(0, slash + 1) + "\">..</a></b><br/>";
                    }
                }

                if (files != null) {
                    for (int i = 0; i < files.length; ++i) {
                        File curFile = new File(f, files[i]);
                        boolean dir = curFile.isDirectory();
                        if (dir) {
                            msg += "<b>";
                            files[i] += "/";
                        }

                        msg += "<a href=\"" + encodeUri(uri + files[i]) + "\">"
                                + files[i] + "</a>";

                        // Show file size
                        if (curFile.isFile()) {
                            long len = curFile.length();
                            msg += " &nbsp;<font size=2>(";
                            if (len < 1024) {
                                msg += len + " bytes";
                            } else if (len < 1024 * 1024) {
                                msg += len / 1024 + "." + (len % 1024 / 10 % 100) + " KB";
                            } else {
                                msg += len / (1024 * 1024) + "." + len % (1024 * 1024) / 10 % 100 + " MB";
                            }

                            msg += ")</font>";
                        }
                        msg += "<br/>";
                        if (dir) {
                            msg += "</b>";
                        }
                    }
                }
                msg += "</body></html>";
                response(HTTPDefinition.HTTPStatus.OK, HTTPDefinition.MIMEType.MIME_HTML, msg, res);
                return;
            } else {
                response(HTTPDefinition.HTTPStatus.OK, HTTPDefinition.MIMEType.MIME_HTML,
                        Localizer.getMessage(Message.NoDirListing), res);
                return;
            }

        }

        try {

            // Get MIME type from file name extension, if possible
            String mime = null;
            int dot = f.getCanonicalPath().lastIndexOf('.');
            if (dot >= 0) {
                mime = (String) HTTPDefinition.getMimeType(f.getCanonicalPath().substring(dot + 1).toLowerCase());
            }
            if (mime == null) {
                mime = HTTPDefinition.MIMEType.MIME_DEFAULT_BINARY;
            }

            // Calculate etag
            String etag = Integer.toHexString((f.getAbsolutePath() + f.lastModified() + "" + f.length()).hashCode());

            // Support (simple) skipping:
            long startFrom = 0;
            long endAt = -1;
            String range = req.header("range");
            if (range != null) {
                if (range.startsWith("bytes=")) {
                    range = range.substring("bytes=".length());
                    int minus = range.indexOf('-');
                    try {
                        if (minus > 0) {
                            startFrom = Long.parseLong(range.substring(0, minus));
                            endAt = Long.parseLong(range.substring(minus + 1));
                        }
                    } catch (NumberFormatException nfe) {
                    }
                }
            }

            // Change return code and add Content-Range header when skipping is requested
            long fileLen = f.length();
            if (range != null && startFrom >= 0) {
                if (startFrom >= fileLen) {
                    response(HTTPDefinition.HTTPStatus.RANGE_NOT_SATISFIABLE, HTTPDefinition.MIMEType.MIME_PLAINTEXT, "", res);
                    res.header("Content-Range", "bytes 0-0/" + fileLen);
                    res.header("ETag", etag);
                } else {
                    if (endAt < 0) {
                        endAt = fileLen - 1;
                    }
                    long newLen = endAt - startFrom + 1;
                    if (newLen < 0) {
                        newLen = 0;
                    }

                    final long dataLen = newLen;
                    FileInputStream fis = new FileInputStream(f) {
                        public int available() throws IOException {
                            return (int) dataLen;
                        }
                    };
                    fis.skip(startFrom);

                    response(HTTPDefinition.HTTPStatus.PARTIALCONTENT, mime, fis, res);
                    res.setInputChannel(Channels.newChannel(fis));
                    res.header("Content-Length", "" + dataLen);
                    res.header("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen);
                    res.header("ETag", etag);
                }
            } else {
                if (etag.equals(req.header("if-none-match"))) {
                    response(HTTPDefinition.HTTPStatus.NOTMODIFIED, mime, "", res);
                } else {
                    response(HTTPDefinition.HTTPStatus.OK, mime, new FileInputStream(f), res);
                    res.header("Content-Length", "" + fileLen);
                    res.header("ETag", etag);
                }
            }

        } catch (IOException ioe) {
            response(HTTPDefinition.HTTPStatus.FORBIDDEN,HTTPDefinition.MIMEType.MIME_PLAINTEXT, Localizer.getMessage(Message.FileAccessRestricited), res);
            return;
        }

        res.header("Accept-Ranges", "bytes");
    }
    
    private void response(String responseCode, String reponseType, String data, HTTPResponse response) {
        if (response == null) {
            return;
        }
        response.response(responseCode);
        response.type(reponseType);
        response.send(data);
    }

    private void response(String responseCode, String reponseType, InputStream is, HTTPResponse response) {
        if (response == null) {
            return;
        }
        response.response(responseCode);
        response.type(reponseType);
        response.setInputChannel(Channels.newChannel(is));
    }

    private String encodeUri(String uri) {
        String newUri = "";
        StringTokenizer st = new StringTokenizer(uri, "/ ", true);
        while (st.hasMoreTokens()) {
            String tok = st.nextToken();
            if (tok.equals("/")) {
                newUri += "/";
            } else if (tok.equals(" ")) {
                newUri += "%20";
            } else {
                try {
                    newUri += URLEncoder.encode(tok, "UTF-8");

                } catch (UnsupportedEncodingException ex) {
                    ;//TODO
                }
            }
        }
        return newUri;
    }
}
